跳到主要内容

MyBatis 关系映射及多表查询

关系类型

表与表之间的关系(relation),分成三种:

  • 一对一(one-to-one):一种对象与另一种对象是一一对应关系,比如一个学生只能在一个班级。
  • 一对多(one-to-many): 一种对象可以属于另一种对象的多个实例,比如一张唱片包含多首歌。
  • 多对多(many-to-many):两种对象彼此都是 "一对多" 关系,比如一张唱片包含多首歌,同时一首歌可以属于多张唱片。

其中,一般通过外键约束它们

  • 一对一:在任意一方引入对方主键作为外键。
  • 一对多:在 “多” 的一方,添加 “一” 的一方的主键作为外键。(连着多条线的一方是 “多”)
  • 多对多:产生中间关系表,引入两张表的主键作为外键,两个主键成为联合主键或使用新的字段作为主键。

MyBatis 加载关联关系对象主要通过两种方式:嵌套查询和嵌套结果。

问题:虽然使用嵌套查询的方式比较简单,但是嵌套查询的方式要执行多条 SQL 语句,这对于大型数据集合和列表展示不是很好,因为这样可能会导致成百上千条关联的 SQL 语句被执行,从而极大的消耗数据库性能并且会降低查询效率。类似暴力 for 循环吧。

解决:MyBatis 延迟加载的配置。使用 MyBatis 的延迟加载在一定程度上可以降低运行消耗并提高查询效率。MyBatis 默认没有开启延迟加载,需要在核心配置文件中的 <settings> 元素内进行配置,具体配置方式如下:

<settings>
<setting name="lazyLoadingEnabled" value="true" />
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

在映射文件中,<association> 元素和 <collection> 元素中都已默认配置了延迟加载属性,即默认属性 fetchType="lazy"(属性 fetchType="eager" 表示立即加载),所以在配置文件中开启延迟加载后,无需在映射文件中再做配置。

懒加载的原理

MyBatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载。在 MyBatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。(上面介绍了)

它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke() 方法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。这就是延迟加载的基本原理。

当然了,不光是 MyBatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。

association 以及 collection 标签

在项目中,某些实体类之间肯定有关键关系,比如一对一,一对多等。在 hibernate 中用 one to oneone to many,而 Mybatis 中就用 association 和 collection。

  • association:一对一关联(has one)
  • collection:一对多关联(has many)

只有在做 select 查询时才会用到这两个标签,它们都有三种用法,且用法类似。

注意:下面所有 resultMap 中的 typeselect 标签中的 resultType 以及 association 中的 javaType,collection 中的 ofType,这里只写了类名,是因为在 mybatis-config.xml 中配置了 typeAliases,否则就要写该类的全类名。

<typeAliases>
<packagename="com.example.entity"/>
</typeAliases>

association 的三种用法

一对一:用 <association> 元素处理(这个是用来关联查询的)

  • a)、property:指定映射到的实体类对象中的属性,与表字段一一对应。
  • b)、column:指定数据库表中对应的字段。
  • c)、javaType:指定映射到实体对象属性的类型。
  • d)、select:指定引入嵌套查询的子SQL语句,该属性用于关联映射中的嵌套查询。
  • e)、fetchType:指定在关联查询时是否启用延迟加载。该属性有lazy和eager两个属性值,默认值为lazy(即默认关联映射延迟加载)。

编写测试环境

创建 Entity

@Data
public class User {
private Integer userId;
private String userName;
private Integer age;
private Card card;//一个人一张身份证,1对1
}
@Data
public class Card {
private Integer cardId;
private String cardNum;//身份证号
private String address;//地址
}

编写 Mapper 接口

public interface UserDao {
/**
* 通过userId查询user信息
* @param userId
* @return
*/
User queryById(int userId);

}
<select id="queryById" parameterType="int" resultMap="userMap">
SELECT u.user_name,u.age,c.card_id,c.card_num,c.address
FROM tb_user u,tb_card c
WHERE u.card_id=c.card_id
AND
u.user_id=#{userId}
</select>

以上是实体类、dao 层的设计以及在 UserDao.xml 中 queryById 方法的 sql 语句的编写,因为不论用 association 的哪种方式, SQL 语句都是一样的写,不同的只是 userMap 的写法,所以这里先给出这段代码。

User 询 Card 是一对一关系,在数据库中,tb_user 表通过外键 card_id 关联 tb_card 表。

下面分别用 association 的三种用法来实现 queryById 方法。

方式一:使用 select 引用

这种方法需要再定义 CardDao.java,如下:

public interface CardDao {
Card queryCardById(int cardId);
}

CardDao.xml 中实现该方法:

<select id="queryCardById" parameterType="int" resultType="Card">
SELECT *
FROM tb_card
WHERE card_id=#{cardId}
</select>

然后再看 UserDao.xml 中是如何引用这个方法的:

<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>

<association property="card"
column="card_id"
select="com.zhu.ssm.dao.
CardDao.queryCardById">
</association>
</resultMap>

在这里直接通过 select 引用 CardDao 的 queryById 方法。个人感觉这种方法比较麻烦,因为还要在 CardDao 里定义 queryCardById 方法并且实现再引用才有用,不过这种方法思路清晰,易于理解。

注意:这里的 association 如果要传递多个参数,可以使用以下方式:

<association column="{param1=id,param2=name}" property="User" select="getUser"></association>

<select id="getUser" resultMap="UserMap" parameterType="java.util.Map">
SELECT * FROM user_table WHERE id = #{param1} and name = #{param2}
</select>

association 标签里面的 column 以对象的形式传过去,接收的时候把 parameterType 改为其中 id 和 name 是对应你表的字段,两个 param 名字随便定义

方式二:嵌套 resultMap

<resultMap type="Card" id="cardMap">
<!-- 注意,这个 Map 是通过这个 id 标签查询到的 -->
<id property="cardId" column="card_id"/>
<result property="cardNum" column="card_num"/>
<result property="address" column="address"/>
</resultMap>


<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>

<association property="card"
resultMap="cardMap">
</association>
</resultMap>

第二种方法就是在 UserDao.xml 中先定义一个 Card 的 resultMap,然后在 User 的 resultMap 的 association 标签中通过 resultMap="cardMap" 引用。这种方法相比于第一种方法较为简单。(注意要在 Card 的 resultMap 加上 id 标签)

这种嵌套的方法细节可以看这个:Mybatis--collection或association嵌套查询(三层或三层以上)

方法三:嵌套 resultMap 简化版

<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>

<association
property="card"
column="card_id"
javaType="Card">
<!-- 同样都是通过这个 id 标签来做关联查询的 -->
<id property="cardId" column="card_id"/>
<result property="cardNum" column="card_num"/>
<result property="address" column="address"/>
</association>
</resultMap>

这种方法就把 Card 的 resultMap 定义在了 association 标签里面,通过 javaType 来指定是哪个类的 resultMap,个人认为这种方法最简单,缺点就是 cardMap 不能复用。具体用哪种方法,视情况而定。

延迟加载

有的同学可能会说,返回的身份证信息我不一定用啊,每次都查询一次数据库,好浪费性能啊,能不能在我使用到身份证信息即获取 card 属性时再去查询数据呢?答案当然是能,那么如何实现呢?

实现延迟加载需要使用 association 标签的 fetchType 属性,该属性有 lazyeager 两个值,分别代表延迟加载和积极加载。

所以上面的配置就要修改成:

<association property="card"
fetchType="lazy"
column="card_id"
select="com.zhu.ssm.dao.
CardDao.queryCardById">
</association>

为了能看到效果,我们在测试方法中添加一行输出语句:

System.out.println("调用 CardDao.queryCardById()");
Assert.assertNotNull(cardDao.queryCardById());

再次运行测试方法,发现输出日志和预期的不一样,在获取 card 属性前还是查询了2次数据库,这是为什么呢?

这是因为 MyBatis 的全局配置中,有一个 aggressiveLazyLoading 参数,如果这个参数的值为 true,会使带有延迟加载属性的对象完整加载,如果为 false,则会按需加载,这个参数默认值为 true(3.4.5版本开始默认值改为 false),而截止目前,我们使用的版本为 3.3.1。

所以我们要在 mybatis-config.xml 中添加如下配置:

<settings>
<!--其他配置-->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

有的同学可能又会说,你现在把全局的 aggressiveLazyLoading 改为了 false,我能不能在触发某个方法时将所有的数据都加载进来呢?答案当然是能,那么如何实现呢?

MyBatis 提供了参数 lazyLoadTriggerMethods,这个参数的含义是,当调用配置中的方法时,加载全部的延迟加载数据,默认值为 “equals,clone,hashCode,toString”

collection 的三种用法

编写测试环境

例如一个土豪有多个手机

User实体类

@Data
public class User{
private Integer userId;
private String userName;
private Integer age;
private List<MobilePhone> mobilePhone;//土豪,多个手机,1对多
}

手机类

@Data
public class MobilePhone {
private Integer mobilePhoneId;
private String brand;//品牌
private double price;//价格
private User user;//主人
}

dao层

public interface UserDao {
/**
* 通过userId查询user信息
* @param userId
* @return
*/
User queryById(int userId);
}

UserDao.xml 中的 select 查询语句

<select id="queryById" parameterType="int" resultMap="userMap">
SELECT u.user_name,u.age,
m.brand,m.price
FROM tb_user u,tb_mobile_phone m
WHERE m.user_id=u.user_id
AND
u.user_id=#{userId}
</select>

方法一:使用 select 引用

这种方法和上面的有点像

先定义 MobilePhoneDao.java

public interface MobilePhoneDao {
List<MobilePhone> queryMbByUserId(int userId);
}

然后实现该方法 MobilePhoneDao.xml

<resultMap type="MobilePhone" id="mobilePhoneMap">
<id property="mobilePhoneId" column="user_id"/>
<result property="brand" column="brand"/>
<result property="price" column="price"/>

<association property="user" column="user_id"
select= "com.zhu.ssm.dao.UserDao.queryById">
</association>
</resultMap>
<select id="queryMbByUserId" parameterType="int" resultMap="mobilePhoneMap">
SELECT brand,price
FROM tb_mobile_phone
WHERE user_id=#{userId}
</select>

做好以上准备工作,那就可以在 UserDao.xml 中引用了

<resultMap type="User" id="userMap">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="age" column="age"/>

<collection property="mobilePhone"
column="user_id"
select="com.zhu.ssm.dao.MobilePhoneDao.queryMbByUserId">
</collection>
</resultMap>

这种方法和 association 的第一种用法几乎是一样的不同之处就是 mobilePhMap 中用到了 association,queryMbByUserId 中要使用 mobilePhoneMap,而不能直接使用 resultType。

方式二:嵌套 resultMap

<resultMap type="MobilePhone" id="mobilephoneMap">
<id column="mobile_phone_id" property="mobilePhoneId"/>
<result column="brand" property="brand" />
<result column="price" property="price" />
</resultMap>
<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<collection property="mobilePhone" resultMap="mobilephoneMap" >
</collection>
</resultMap>

定义好这两个 resultMap,再引用 UserMap 就行了。

方法三:嵌套 resultMap 简化版

<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<collection property="mobilePhone"
column="user_id"
ofType="MobilePhone">

<id column="mobile_phone_id" property="mobilePhoneId" />
<result column="brand" property="brand" />
<result column="price" property="price" />
</collection>
</resultMap>

这种方法需要注意,一定要有 ofType,collection 装的元素类型是啥 ofType 的值就是啥,这个一定不能少。

一对一关系

class A{
B b;
}

class B{
A a;
}
//一对一:在本类中定义对方类型的对象,如 A 类中定义 B 类类型的属性 b,B 类中定义 A 类类型的属性 a

举例:以个人和身份证之间的一对一关联关系

创建测试表

创建两个表 tb_idcardtb_person

USE mybatis;
CREATE TABLE tb_idcard(
id INT PRIMARY KEY AUTO_INCREMENT,
CODE VARCHAR(18)
);
INSERT INTO tb_idcard(CODE) VALUE('152221198711020624');
INSERT INTO tb_idcard(CODE) VALUE('152201199008150317');

CREATE TABLE tb_person(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(32),
age INT,
sex VARCHAR(8),
card_id INT UNIQUE,
FOREIGN KEY(card_id) REFERENCES tb_idcard(id)
);
INSERT INTO tb_person(NAME,age,sex,card_id) VALUE('Rose',29,'女',1);
INSERT INTO tb_person(NAME,age,sex,card_id) VALUE('tom',27,'男',2);

此时表内数据:

创建持久化类

package com.itheima.po;

/**
* 证件持久化类
*/
@Data
public class IdCard {
private Integer id;
private String code;
}
package com.itheima.po;

/**
* 个人持久化类
*/
@Data
public class Person {
private Integer id;
private String name;
private Integer age;
private String sex;
private IdCard card; //个人关联的证件
}

方法一:嵌套查询

IdCardMapper.xml 映射文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.IdCardMapper">

<!-- 根据id查询证件信息,最普通的配置信息 -->
<select id="findCodeById" parameterType="Integer" resultType="IdCard">
SELECT * from tb_idcard where id=#{id}
</select>

</mapper>

PersonMapper.xml 映射文件:

<!-- 嵌套查询:通过执行另外一条SQL映射语句来返回预期的特殊类型 -->
<select id="findPersonById" parameterType="Integer"
resultMap="IdCardWithPersonResult123">
SELECT * from tb_person where id=#{id}
</select>

<!-- resultMap最终还是要将结果映射到pojo上,type就是指定映射到哪一个pojo -->
<resultMap type="Person" id="IdCardWithPersonResult123">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="age" column="age" />
<result property="sex" column="sex" />

<!-- 注意这是在同一个 resultMap 里面 -->
<!-- 一对一:association使用select属性引入另外一条SQL语句,是另一个映射文件的select元素id -->
<association property="card" column="card_id" javaType="IdCard"
select="com.itheima.mapper.IdCardMapper.findCodeById" />
</resultMap>

嵌套查询:测试方法

测试方法:

/**
* 嵌套查询
*/
@Test
public void findPersonByIdTest() {
// 1、通过工具类生成SqlSession对象
SqlSession session = MybatisUtils.getSession();
// 2.使用MyBatis嵌套查询的方式查询id为1的人的信息
Person person = session.selectOne("com.itheima.mapper."
+ "PersonMapper.findPersonById", 1);
// 3、输出查询结果信息
System.out.println(person);
// 4、关闭SqlSession
session.close();
}

运行结果:执行了多条简单的 SQL 语句

方法二:嵌套结果的方式

实际上就是直接查询两个表,合并它们的结果

PersonMapper.xml 映射文件

<!-- 嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集 -->
<select id="findPersonById2" parameterType="Integer"
resultMap="IdCardWithPersonResult2">
SELECT p.*,idcard.code
from tb_person p,tb_idcard idcard
where p.card_id=idcard.id
and p.id= #{id}
</select>

<resultMap type="Person" id="IdCardWithPersonResult2">
<id property="id" column="id" /><!-- 声明主键,id是关联查询对象的唯一标识符 -->
<result property="name" column="name" />
<result property="age" column="age" />
<result property="sex" column="sex" />
<association property="card" javaType="IdCard">
<id property="id" column="card_id" />
<result property="code" column="code" />
</association>
</resultMap>

嵌套结果:测试方法

/**
* 嵌套结果
*/
@Test
public void findPersonByIdTest2() {
// 1、通过工具类生成SqlSession对象
SqlSession session = MybatisUtils.getSession();
// 2.使用MyBatis嵌套结果的方法查询id为1的人的信息
Person person = session.selectOne("com.itheima.mapper."
+ "PersonMapper.findPersonById2", 1);
// 3、输出查询结果信息
System.out.println(person);
// 4、关闭SqlSession
session.close();
}

测试结果:只执行了一条复杂的 SQL 语句。

DEBUG [main] - ==> Preparing: SELECT p.*,idcard.code from tb_person p,tb_idcard idcard where p.card_id=idcard.id and p.id= ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person [id=1, name=Rose, age=29, sex=女, card=IdCard [id=1, code=152221198711020624]]

实际上就是直接查询两个表,合并它们的结果

select p.*,idcard.code from tb_person p,tb_idcard idcard where p.card_id = idcard.id and p.id = #{id}

一对一关系配置模板

一对多关系

class A{
List<B> b;
}
class B{
A a;
}
//一对多:就是一个 A 类类型中对应多个 B 类类型的情况,
// 需要在 A 类中以集合的方式引入 B 类类型的对象,在 B 类中定义 A 类类型的属性 a

一对多关系就是 <resultMap> 元素中,包含一个子元素 <collection> 元素,属性大部分和 <association> 元素相同,但有一个特殊属性 ofType,这个属性和 javaType 属性对应,用于指定实体对象中集合类属性所包含的元素类型。

一对多关系配置模板

<collection> 元素的使用模板:

创建测试表

在MySQL中建表 tb_usertb_orders,并插入几条数据

USE mybatis;

CREATE TABLE tb_user(
id INT(32) PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(32),
address VARCHAR(256)
);
INSERT INTO tb_user VALUES('1','詹姆斯','克利夫兰');
INSERT INTO tb_user VALUES('2','科比','洛杉矶');
INSERT INTO tb_user VALUES('3','保罗','洛杉矶');

USER mybatis;
CREATE TABLE tb_orders(
id INT(32) PRIMARY KEY AUTO_INCREMENT,
number VARCHAR(32) NOT NULL,
user_id INT(32) NOT NULL,
FOREIGN KEY(user_id) REFERENCES tb_user(id)
);
INSERT INTO tb_orders VALUES('1','1000011','1');
INSERT INTO tb_orders VALUES('2','1000012','1');
INSERT INTO tb_orders VALUES('3','1000013','2');

结果:

创建持久化类

package com.itheima.po;
import java.util.List;
/**
* 用户持久化类
*/
@Data
public class User {
private Integer id; // 用户编号
private String username; // 用户姓名
private String address; // 用户地址
private List<Orders> ordersList; //用户关联的订单...................................
}
package com.itheima.po;

import java.util.List;

/**
* 订单持久化类
*/
@Data
public class Orders {
private Integer id; //订单id
private String number;//订单编号
//关联商品集合信息
private List<Product> productList;
}

创建映射文件

创建映射文件 UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace表示命名空间 -->
<mapper namespace="com.itheima.mapper.UserMapper">

<!-- 一对多:查看某一用户及其关联的订单信息
注意:当关联查询出的列名相同,则需要使用别名区分 -->
<select id="findUserWithOrders123" parameterType="Integer"
resultMap="UserWithOrdersResult123">
SELECT u.*,o.id as orders_id,o.number
from tb_user u,tb_orders o
WHERE u.id=o.user_id
and u.id=#{id}
</select>

<resultMap type="User" id="UserWithOrdersResult123">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="address" column="address"/>

<!-- 一对多关联映射:collection
ofType表示属性集合中元素的类型,List<Orders>属性即Orders类 -->
<collection property="ordersList" ofType="Orders">
<id property="id" column="orders_id"/>
<result property="number" column="number"/>
</collection>

</resultMap>
</mapper>

编写测试方法

/**
* 一对多
*/
@Test
public void findUserTest() {
// 1、通过工具类生成SqlSession对象
SqlSession session = MybatisUtils.getSession();
// 2、查询id为1的用户信息
User user = session.selectOne("com.itheima.mapper."
+ "UserMapper.findUserWithOrders123", 1);
// 3、输出查询结果信息
System.out.println(user);
// 4、关闭SqlSession
session.close();
}

查询结果:

DEBUG [main] - ==> Preparing: SELECT u.*,o.id as orders_id,o.number from tb_user u,tb_orders o WHERE u.id=o.user_id and u.id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 2
User [id=1, username=詹姆斯, address=克利夫兰, ordersList=[Orders [id=1, number=1000011, productList=null], Orders [id=2, number=1000012, productList=null]]]

多对多关系

class A{
List<B> b;
}
class B{
List<A> a;
}
//多对多:在 A 类中定义 B 类类型的集合,在 B 类中定义 A 类类型的集合

以商品和订单为例,一个订单可以包含多个商品,一个商品可以属于多个订单,数据库中的多对多联系通常使用一个中间表来维护,中间表中的订单id 作为外键参照订单表的id,商品id 作为外键参照商品表的id。

外键:这个表的某一属性是别的表的主键,可以有重复,可以有多个,可以为空

创建测试表

tb_producttb_ordersitem

USE mybatis;

CREATE TABLE tb_product(
id INT(32) PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(32),
price DOUBLE
);
INSERT INTO tb_product VALUES('1','Java基础入门','44.5');
INSERT INTO tb_product VALUES('2','Java Web程序开发入门','38.5');
INSERT INTO tb_product VALUES('3','SSM框架整合实战','50');

CREATE TABLE tb_ordersitem(
id INT(32) PRIMARY KEY AUTO_INCREMENT,
orders_id INT(32),
product_id INT(32),
FOREIGN KEY(orders_id) REFERENCES tb_orders(id),
FOREIGN KEY(product_id) REFERENCES tb_product(id)
);
INSERT INTO tb_ordersitem VALUES('1','1','1');
INSERT INTO tb_ordersitem VALUES('2','1','3');
INSERT INTO tb_ordersitem VALUES('3','3','3');

建表后:

创建持久类

创建持久化类 Product 表示商品,订单用以前的 Order

package com.itheima.po;
import java.util.List;
/**
* 商品持久化类
*/
@Data
public class Product {
private Integer id; //商品id
private String name; //商品名称
private Double price;//商品单价
private List<Orders> orders; //与订单的关联属性...........................
}

创建映射文件

创建订单实体映射文件 OrdersMapper.xmlProductMapper.xml

OrdersMapper.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.OrdersMapper">

<!-- 多对多嵌套查询:通过执行另外一条SQL映射语句来返回预期的特殊类型
定义了一个id为findOrdersWithPorduct的select语句来查询订单及其关联的商品信息-->
<select id="findOrdersWithPorduct" parameterType="Integer"
resultMap="OrdersWithProductResult">
select * from tb_orders WHERE id=#{id}
</select>

<resultMap type="Orders" id="OrdersWithProductResult">
<id property="id" column="id" />
<result property="number" column="number" />

<!-- property属性表示订单持久化类中的商品属性,ofType属性表示集合中的数据为Product类型,而column的
属性值会作为参数执行ProductMapper中定义的id为findProductById的执行语句来查询订单中的商品信息-->
<collection property="productList" column="id" ofType="Product"
select="com.itheima.mapper.ProductMapper.findProductById">
</collection>

</resultMap>



<!-- 多对多嵌套结果查询:查询某订单及其关联的商品详情 -->
<select id="findOrdersWithPorduct2" parameterType="Integer"
resultMap="OrdersWithPorductResult2">
select o.*,p.id as pid,p.name,p.price
from tb_orders o,tb_product p,tb_ordersitem oi
WHERE oi.orders_id=o.id
and oi.product_id=p.id
and o.id=#{id}
</select>

<!-- 自定义手动映射类型 -->
<resultMap type="Orders" id="OrdersWithPorductResult2">
<id property="id" column="id" />
<result property="number" column="number" />
<!-- 多对多关联映射:collection -->
<collection property="productList" ofType="Product">
<id property="id" column="pid" />
<result property="name" column="name" />
<result property="price" column="price" />
</collection>
</resultMap>

</mapper>

ProductMapper.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.ProductMapper">
<select id="findProductById" parameterType="Integer"
resultType="Product">
SELECT * from tb_product where id IN(
SELECT product_id FROM tb_ordersitem WHERE orders_id = #{id}
)
</select>
</mapper>

编写测试方法

/**
* 多对多
*/
@Test
public void findOrdersTest(){
// 1、通过工具类生成SqlSession对象
SqlSession session = MybatisUtils.getSession();
// 2、查询id为1的订单中的商品信息
Orders orders = session.selectOne("com.itheima.mapper."
+ "OrdersMapper.findOrdersWithPorduct", 1);
// 3、输出查询结果信息
System.out.println(orders);
// 4、关闭SqlSession
session.close();
}

测试结果:

DEBUG [main] - ==> Preparing: select * from tb_orders WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
DEBUG [main] - ==> Preparing: SELECT * from tb_product where id IN( SELECT product_id FROM tb_ordersitem WHERE orders_id = ? )
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 2
Orders [id=1, number=1000011, productList=[Product [id=1, name=Java基础入门, price=44.5], Product [id=3, name=SSM框架整合实战, price=50.0]]]

@One 一对一映射

以获取用户的唯一地址为例,首先我们定义一个根据地址 id 查询地址的查询方法

@Mapper
public interface AddressRepository {
/**
* 根据地址id查询地址
*/
@Select("SELECT * FROM `address` WHERE id = #{id}")
AddressPO findAddressById(Long id);
}

然后我们定义一个根据用户 id 查询用户的方法

@Mapper
public interface MySqlUserRepository {

@Select("SELECT * FROM `user` WHERE id = #{id}")
UserPO find(Long id);

}

这个时候我们查询出来的 user 对象中的 address 属性是空的,和 address 并没有任何关联。

那么我们要把 user 中的 addressId 传递给 AddressRepository 的查询地址的方法,然后把查询出的地址对象 address 赋值给 user 的 address 属性,那么我们怎么做呢?

@Mapper
public interface MySqlUserRepository {

@Select("SELECT * FROM `user` WHERE id = #{id}")
@Results({
@Result(property = "address", column = "address_id",
one = @One(select = "com.kimzing.data.repository.AddressRepository.findAddressById"))
})
UserPO find(Long id);

}

我们要使用 @Result 注解对返回的结果进行配置

  • property = "address" 表示要将返回的查询结果赋值给 user 的 address 属性
  • column = "address_id" 是指将 user 表中的 address_id 作为 com.kimzing.data.repository.AddressRepository.findAddressById 的查询参数
  • one 表示这是一个一对一的查询
  • @One(select = "方法全路径") 表示我们调用的方法

注解的一对多查询 @Many

@Mapper
public interface MySqlUserRepository {

@Select("SELECT * FROM `user` WHERE id = #{id}")
@Results({
@Result(property = "address", column = "address_id",
one = @One(select = "com.kimzing.data.repository.AddressRepository.findAddressById")),
@Result(property = "cars", column = "id",
many = @Many(select = "com.kimzing.data.repository.CarRepository.findCarsByUserId"))
}),
// 对userId进行赋值
@Result(property = "id", column = "id")
UserPO find(Long id);
}

我们要使用 @Result 注解对返回的结果进行配置

  • property = "cars",表示要将返回的查询结果赋值给 user 的 cars 属性
  • column = "id" 是指将 user 表中的用户主键 id 作为 com.kimzing.data.repository.CarRepository.findCarsByUserId 的查询参数
  • many 表示这是一个一对多的查询
  • @Many(select = "方法全路径") 表示我们调用的方法, 方法参数userId就是上面column指定的列值

Reference

参考资料 MyBatis系列(十):使用association标签实现嵌套查询 参考资料 阮一峰老师的 ORM 实例教程 参考资料 mybatis的association以及collection的用法 参考资料 守林鸟-第9章 MyBatis的关系映射